/*- -*- mode: c; c-basic-offset: 8; -*-
 *
 * proxy.c - CS:APP Web proxy
 *
 * TEAM MEMBERS:
 *     Damien Stone, djs4@rice.edu 
 *     Frank Salinas, fs5@rice.edu
 * 
 */ 

#include "csapp.h"
#include <string.h>
#include <pthread.h>

/*
 * Function prototypes
 */
int	parse_uri(char *uri, char *target_addr, char *path, int *port);
void	format_log_entry(char *logstring, struct sockaddr_in *sockaddr,
	    char *uri, int size);

int processclient(int connfd, struct sockaddr_in clientaddr);
int open_clientfd_ts(char *host, int port);
void *thread(void* vargp);

/* Thread stuff?*/
pthread_mutex_t gethost;
pthread_mutex_t logmut;
pthread_t tid;

/* rio wrapper prototypes */
ssize_t Rio_readlineb_w(rio_t *rp, void *usrbuf, size_t maxlen);
void Rio_writen_w(int fd, void *usrbuf, size_t n);

/* A structure for passing into subthreads that process clients.
 * The fields MUST BE FREED after the subthread is done using them.
 */
struct clientdata {
	int *desc; /* The file descriptor */
	struct sockaddr_in *clientaddr; /* A pointer to the client's sockaddr_in */
};

/* 
 * main - Main routine for the proxy program 
 */
int main(int argc, char **argv) 
{
	int listenfd, *connfdp, port;
	socklen_t clientlen;
	struct sockaddr_in *clientaddr;
	struct clientdata *cd;

	if (argc != 2) {
		fprintf(stderr, "usage: %s <port>\n", argv[0]);
		exit(0);
	}

	/*Initialize Signal handling?*/
	Signal(SIGPIPE, SIG_IGN);
	/*Initialize mutexs*/
	if (pthread_mutex_init(&gethost, NULL) != 0 || pthread_mutex_init(&logmut, NULL) != 0) {
		printf("Error initializing a mutex!\n");
		exit(-1);
	}
	port = atoi(argv[1]);
	
	listenfd = Open_listenfd(port);
	while (1) {
		clientlen = sizeof(clientaddr);

		cd = Malloc(sizeof(struct clientdata));
		connfdp = Malloc(sizeof(int));
		clientaddr = Malloc(clientlen);

		*connfdp = Accept(listenfd, (SA *)clientaddr, &clientlen);
		cd->desc = connfdp;
		cd->clientaddr = clientaddr;
		
		/* Leave it to the thread to free resources */
		Pthread_create(&tid, NULL, thread, cd);
	
	}
	if (pthread_mutex_destroy(&gethost) != 0 || pthread_mutex_destroy(&logmut) != 0) {
		printf("Error destroying mutex!\n");
	}
	exit(0);
        
}


/*
 * parse_uri - URI parser
 * 
 * Requires: 
 *   The memory for hostname and pathname must already be allocated
 *   and should be at least MAXLINE bytes.  Port must point to a
 *   single integer that has already been allocated.
 *
 * Effects:
 *   Given a URI from an HTTP proxy GET request (i.e., a URL), extract
 *   the host name, path name, and port.  Return -1 if there are any
 *   problems.
 */
int
parse_uri(char *uri, char *hostname, char *pathname, int *port)
{
	char *hostbegin;
	char *hostend;
	char *pathbegin;
	int len;

	if (strncasecmp(uri, "http://", 7) != 0) {
		hostname[0] = '\0';
		return (-1);
	}
       
	/* Extract the host name. */
	hostbegin = uri + 7;
	hostend = strpbrk(hostbegin, " :/\r\n\0");
	len = hostend - hostbegin;
	strncpy(hostname, hostbegin, len);
	hostname[len] = '\0';
    
	/* Extract the port number. */
	*port = 80; /* default */
	if (*hostend == ':')   
		*port = atoi(hostend + 1);
    
	/* Extract the path. */
	pathbegin = strchr(hostbegin, '/');
	if (pathbegin == NULL)
		strcpy(pathname, "/");
	else
		strcpy(pathname, pathbegin);

	return (0);
}

/*
 * format_log_entry - Create a formatted log entry in logstring. 
 *
 * Requires:
 *   The memory for logstring must already be allocated and should be
 *   at least MAXLINE bytes.  Sockaddr must point to an allocated
 *   sockaddr_in structure.  Uri must point to a properly terminated
 *   string.
 *
 * Effects:
 *   A properly formatted log entry is stored in logstring using the
 *   socket address of the requesting client (sockaddr), the URI from
 *   the request (uri), and the size in bytes of the response from the
 *   server (size).
 */
void
format_log_entry(char *logstring, struct sockaddr_in *sockaddr, char *uri,
    int size)
{
	time_t now;
	char time_str[MAXLINE];
	unsigned long host;
	unsigned char a, b, c, d;

	/* Get a formatted time string. */
	now = time(NULL);
	strftime(time_str, MAXLINE, "%a %d %b %Y %H:%M:%S %Z",
	    localtime(&now));

	/*
	 * Convert the IP address in network byte order to dotted decimal
	 * form.  Note that we could have used inet_ntoa, but chose not to
	 * because inet_ntoa is a Class 3 thread unsafe function that
	 * returns a pointer to a static variable (Ch 13, CS:APP).
	 */
	host = ntohl(sockaddr->sin_addr.s_addr);
	a = host >> 24;
	b = (host >> 16) & 0xff;
	c = (host >> 8) & 0xff;
	d = host & 0xff;

	/* Return the formatted log entry string */
	sprintf(logstring, "%s: %d.%d.%d.%d %s %d", time_str, a, b, c, d, uri,
	    size);
}


/* Thread routine
 * Requires: vargp be a pointer to an int returned by Accept
 *
 * Effects: Has the thread execute processclient
 *
 */
void*
thread(void* vargp)
{
	struct clientdata cd = *((struct clientdata *)vargp);
	
	Pthread_detach(pthread_self());
	
	processclient(*(cd.desc), *(cd.clientaddr));
	Close(*(cd.desc));

	Free(cd.desc);
	Free(cd.clientaddr);
	Free(vargp);
	
	return NULL;
}


/*
 * Requires:
 *   connfd is a valid connection file descriptor to a connected client.
 *
 * Effects:
 *   Takes the request from the browser and forwards the data to the server
 *   they ask about. Then returns the server's answer to the client. Also logs
 *   this request. Returns a 0 on success, -1 on failure.
 */
int
processclient(int connfd, struct sockaddr_in clientaddr)
{
	int *portnum, serverfd;
	char buf[MAXLINE], pathname[MAXLINE];
	char *getreq, *uri, *hostname;
	size_t n, urisize, responsesize;
	rio_t clientrio, serverrio;
	FILE *logfile;
		
	Rio_readinitb(&clientrio, connfd);

	/* Read the get request to find our server */
	n = Rio_readlineb(&clientrio, buf, MAXLINE);

	/* If it's not a get request, we can't process */
	if (strncasecmp(buf, "get", 3) != 0) {
		printf("Error: Client tried to send a non-GET request!\n");
		printf("Ending connection.\n");
		return (-1);
	}
	
	uri = Malloc(MAXLINE * sizeof(char));
	portnum = Malloc(sizeof(int));
	getreq = Malloc(MAXLINE * sizeof(char));
	hostname = Malloc(MAXLINE * sizeof(char));

	/* Get the URI here
	 * Subtract to hopefully account for "GET " and "HTTP/1.1\n"
	 */
	urisize = (size_t)(n - 14 * sizeof(char));
        strncpy(uri, buf + 4 * sizeof(char), urisize);
	uri[urisize/sizeof(char)] = '\0';

	printf("%s\n", uri);

	if(parse_uri(uri, hostname, pathname, portnum) == -1) {
		printf("URI parsing error!\n");
		Free(uri);
		Free(portnum);
		Free(getreq);
		Free(hostname);
		return -1;
	}

	if ((serverfd = open_clientfd_ts(hostname, *portnum)) < 0) {
		printf("Unable to contact server!\n");
		Free(uri);
		Free(portnum);
		Free(getreq);
		Free(hostname);
		return -1;
	}
	Rio_readinitb(&serverrio, serverfd);

	strcpy(getreq, "GET ");
	strcat(getreq, pathname);
	strcat(getreq, " HTTP/1.0\n");
	
	/* Send the first header we created, then others from the client */
	Rio_writen_w(serverfd, getreq, strlen(getreq));
	printf("%s", getreq);
	
	while((n = Rio_readlineb(&clientrio, buf, MAXLINE)) != 0) {
		
		/* Make sure we don't try to keep the connection alive */
		if(strncasecmp(buf, "keep-alive:", 11) == 0 || strncasecmp(buf, "proxy-connection:", 17) == 0) {
			continue;
		}
		Rio_writen_w(serverfd, buf, n);
		printf("%s", buf);
		
		/* Check if a blank line has been written, so server time */
		if (n == 2) {
			break;
		}
	}

	responsesize = 0;

	while((n = Rio_readlineb(&serverrio, buf, MAXLINE)) != 0) {
		Rio_writen_w(connfd, buf, strlen(buf));
		responsesize += n;
		printf("%s", buf);
		/* Check if a blank line has been written, end of headers */
		if (n == 2) {
			break;		
		}
	}
	
	while((n = Rio_readnb(&serverrio, buf, MAXLINE)) != 0) {
		Rio_writen_w(connfd, buf, n);
		responsesize += n;
	}

	/* Since the server is done writing, we can use buf for the log */
	format_log_entry(buf, &clientaddr, uri, responsesize);

	/* Free everything except the uri, which we still need */
	Free(portnum);
	Free(getreq);
	Free(hostname);
	Close(serverfd);

	/* Acquire the lock to prevent log file corruption */
	if (pthread_mutex_lock(&logmut) != 0) {
		printf("Acquiring log mutex failed!\nNot logging!\n");
		Free(uri);
		return -1;
	}
	
	if ((logfile = fopen("proxy.log", "a")) == NULL) {
		printf("Error opening log file!\n");
	}

	fprintf(logfile, "%s\n", buf);

	if (fclose(logfile) != 0) {
		printf("Error closing log file!\n");
	}

	if (pthread_mutex_unlock(&logmut) != 0) {
		printf("Releasing log mutex failed!\n");
	}

	printf("End of transaction, freed memory and logged.\n\n");
	Free(uri);
	return (0);
	
}


/*
 * Requires:
 * Nothing?
 *
 * Effects:
 *   Thread-safe implementation of open_clientfd in csapp. 
 *   Opens a connection to server at <host, port>
 */
int
open_clientfd_ts(char *host, int port){
	int clientfd;
	struct hostent *hp;
	struct sockaddr_in serveraddr;
	/* Unix Error. Check errnor for cause of error */
	if((clientfd = socket(AF_INET, SOCK_STREAM,0)) < 0)
		return -1;

	/*Fill in the server's IP address and port*/
	/*get the mutex*/
	if (pthread_mutex_lock(&gethost) != 0) {
		printf("Error acquiring mutex!\n");
		return -1;
	}
	
	/*DNS error*/
	if((hp = gethostbyname(host)) == NULL)
		return -2; 
	bzero((char *) &serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	bcopy((char *) hp -> h_addr,
	      (char *) &serveraddr.sin_addr.s_addr, hp->h_length);
	serveraddr.sin_port = htons(port);

	/*Establish connection w server */
	if(connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) <0 )
		return -1;

	if (pthread_mutex_unlock(&gethost) != 0) {
		printf("Error releasing mutex!\n");
	}
	return clientfd;
}

/*
 * A wrapper method around rio_readlineb that prints a warning if it fails,
 * instead of exiting like Rio_readlineb
 */
ssize_t
Rio_readlineb_w(rio_t *rp, void *usrbuf, size_t maxlen)
{
	ssize_t rc;

	if ((rc = rio_readlineb(rp, usrbuf, maxlen)) < 0) {
		printf("An error occurred while reading data from rio_readlineb!\n");
		printf("%s\n", strerror(errno));
		return 0;
	}
	
	return rc;
}

/*
 * A wrapper method around rio_writen that prints a warning if it fails,
 * instead of exiting like Rio_writen
 */
void
Rio_writen_w(int fd, void *usrbuf, size_t n)
{
	if (rio_writen(fd, usrbuf, n) != (ssize_t)(n)) {
		printf("An error occurred while trying to write with rio_writen!\n");
		printf("%s\n", strerror(errno));
	}
}

/*
 * A wrapper method around rio_readnb that prints a warning if it fails,
 * instead of exiting like Rio_readnb
 */
ssize_t
Rio_readnb_w(rio_t *rp, void *usrbuf, size_t n)
{
	ssize_t rc;
	
	if ((rc = rio_readnb(rp, usrbuf, n)) < 0) {
		printf("An error occurred while trying to read with rio_readnb!\n");
		printf("%s\n", strerror(errno));
		return 0;
	}
	return rc;
}
